#! /usr/bin/python

import getopt
import os
import sys

# This is the first thing that I ever wrote in Python, and was written around
# when Python's latest version was 2.7 or so. So, probably 2007 or so.
# The goal of this project was to have a tool that would dive into directories
# and remove all of the trailing space that was inside of them. At the end, it
# also provides a count of how many trailing spaces have been removed.
# This could probably be replaced by a complex one-liner in either sed, perl,
# or something like that.
# I've decided to commit it as I found it, including the broken stuff. This is
# so that anyone looking at the repository history can see what I would have
# done differently (how my skills have improved).
# This project was fairly small, and so it was not initially under source
# control. I regret that.

# Right now, this would work fine as a dict. However, I intent to add some class
# members fairly soon.

class despacer():
    # Directories that the user wants to avoid.
    ignore_dirs = []
    # The number of errors that have occured.
    errors = 0
    # Total bytes removed.
    total_removed = 0
    # True if I/O errors shouldn't be printed.
    silent_errors = False
    # If True, don't remove anything.
    dry_run = False
    # Extensions that the user wishes to process. Other extensions will be
    # ignored.
    extensions = []

    def __init__(self):
        pass

    def strip(self, path):
        # Read in a file, then replace it.
        try:
            f = open(path, 'r')
        except IOError, (errno):
            if self.silent_errors == False:
                print '(I/O Error, %s from %s)' % (errno, path)
            self.errors += 1
            return

        buff = []

        # How many were removed from the file.
        removed = 0

        for line in f.xreadlines():
            count = len(line)

            # Figure out the final bit of the line.
            lineTail = []

            # Determine what newlines are at the end of the file. These won't be
            # stripped, so adjust the count accordingly.

            if line[-1] == '\n':
                if count > 1 and line[-2] == '\r':
                    lineTail = '\r\n'
                    count -= 2
                else:
                    lineTail = '\n'
                    count -= 1
            elif line[-1] == '\r':
                lineTail = '\r'
                count -= 1
            else:
                lineTail = ''

            line = line.rstrip(' \t\r\n')
            newCount = len(line)

            if newCount != count:
                removed += count - newCount

            buff.append(line + lineTail)

        f.close()
        if removed == 0:
            # If nothing was removed, don't bother the user.
            return

        if not self.dry_run:
            try:
                f = open(path, 'w')
                for i in range(len(buff)):
                    f.write(buff[i])
            except IOError, (errno):
                if self.silent_errors == False:
                    print '(I/O Error, %s from %s)' % (errno, path)
                self.errors += 1
                return

        f.close()

        # Add up how many have been removed so far, and print how many came from
        # this file.
        self.total_removed += removed
        print '%d from %s' % (removed, path)

    def checkDirectory(self, directory):
        print 'despacer: Scanning %s' % directory
        for root, dirs, files in os.walk(directory, topdown=True):
            for d in self.ignore_dirs:
                try:
                    dirs.remove(d)
                except ValueError:
                    pass

            # Check the names to make sure they're okay.
            for name in files:
                # This makes it so that x.y.z comes as x and .y.z
                # This allows --extensions= to specify complex types like
                # .tar.gz (No, don't actually do that).
                last_ext = name.split('.', 1)[-1]

                if last_ext in self.extensions:
                    self.strip(os.path.join(root, name))

def usage():
    message = \
    """
    despacer.py:

    This is used to remove trailing spaces and tabs from files while preserving
    their line endings. This will recurse into directories that it's given. It
    prints out how much extra spacing is at the end of each file it processes,
    as well as the total amount of extra spacing.

    -h/--help       : This help screen.
    --dir=d         : Adds a single directory to be processed.
                      This can be an absolute path, or a path relative to the
                      current directory. ~ is replaced with the current user's
                      home directory.
    --extensions=e  : A comma-separated list of extensions to process. These can
                      start with a dot or not (ex: .txt and txt are the same).
                      Files that don't include a dot will also work
                      (ex: Makefiles).
    --ignore-dir=d  : Adds a single directory to be avoided.
    --dry-run       : Prints stats, but does not modify files.
    --silent-errors : Don't print I/O errors occured when trying to open, read,
                      or write a file. Errors will still be printed along with
                      a count at program exit.
    """

    print message
    sys.exit(2)

def main(ds, argv = None):
    # Take the args from sys.argv if they're not specified.
    if argv is None:
        argv = sys.argv

    try:
        opts, args = getopt.getopt(sys.argv[1:], 'h',
                                   ['extensions=', 'help', 'silent-errors',
                                    'dry-run', 'dir=', 'ignore-dir='])

    except getopt.GetoptError, err:
        # print help information and exit:
        print 'despacer: Error: %s' % str(err)
        usage()

    dirs = []

    for o, a in opts:
        if o == '--extensions':
            exts = a.split(',')
            for i in xrange(len(exts)):
                e = exts[i]
                try:
                    if e[0] == '.':
                        exts[i] = e[1:]
                except IndexError:
                    # Raised by e[0] reaching too far since e is blank.
                    # Safe to ignore this.
                    pass

            ds.extensions += exts
        elif o in ('-h', '--help'):
            usage()
        elif o == '--silent-errors':
            ds.silent_errors = True
        elif o == '--dry-run':
            ds.dry_run = True
        elif o == '--ignore-dir':
            # Have to use append this time because 'a' is a string, not a fellow
            # list.
            ds.ignore_dirs.append(a)
        elif o == '--dir':
            if a == '':
                continue

            path = ''
            # First, determine what kind of path this is.
            if os.path.isabs(a):
                # /... or C:, so use that and ignore the current dir.
                path = os.path.normpath(a)
            elif a[0] == '~':
                # ~/, so use the user's home and treat as absolute.
                path = os.path.expanduser(a)
            else:
                # Other, so treat as relative to the current dir.
                path = os.path.abspath(a)

            dirs.append(path)
        else:
            assert False, 'unhandled option'

    for d in dirs:
        ds.checkDirectory(d)

if __name__ == '__main__':
    ds = despacer()
    main(ds)
    print 'despacer: Removed %d bytes of extra space with %d errors.' % (ds.total_removed, ds.errors)

